/*
 * Copyright (C) 2018-2023 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
*/

"use strict";

function computeIsLittleEndian() {
    let buf = new ArrayBuffer(4);
    let dv = new DataView(buf);
    dv.setUint32(0, 0x11223344, true);
    let view = new Uint8Array(buf);
    return view[0] === 0x44;
}

const isLittleEndian = computeIsLittleEndian();

async function *randomFileContents() {
    let counter = 1;
    while(true) {
        const numBytes = ((counter * 1192.18851371) % 2056);
        counter++;
        let result = new ArrayBuffer(numBytes);
        let view = new Uint8Array(result);
        for (let i = 0; i < numBytes; ++i)
            view[i] = (i + counter) % 255;
        yield new DataView(result);
    }
}


class File {
    constructor(dataView, permissions) {
        this._data = dataView;
    }

    get data() { return this._data; }

    set data(dataView) { this._data = dataView; }

    get byteLength() { return this._data.byteLength; }

    swapByteOrder() {
        let hash = 0x1a2b3c4d;
        for (let i = 0; i < Math.floor(this.data.byteLength / 8) * 8; i += 8) {
            const data = this.data.getFloat64(i, isLittleEndian);
            this.data.setFloat64(i, data, !isLittleEndian);
            hash ^= data | 0;
        }
        return hash;
    }
}

class Directory {
    constructor() {
        this.structure = new Map;
    }

    async addFile(name, file) {
        let entry = this.structure.get(name);
        if (entry !== undefined) {
            if (entry instanceof File)
                throw new Error("Can't replace file with file.");
            if (entry instanceof Directory)
                throw new Error("Can't replace a file with a new directory.");
            throw new Error("Should not reach this code");
        }

        this.structure.set(name, file);
        return file;
    }

    async addDirectory(name, directory = new Directory) {
        let entry = this.structure.get(name);
        if (entry !== undefined) {
            if (entry instanceof File)
                throw new Error("Can't replace file with directory.");
            if (entry instanceof Directory)
                throw new Error("Can't replace directory with new directory.");
            throw new Error("Should not reach this code");
        }

        this.structure.set(name, directory);
        return directory;
    }

    async* ls() {
        for (let [name, entry] of this.structure)
            yield { name, entry, isDirectory: entry instanceof Directory };
    }

    async* forEachFile() {
        for await (let item of this.ls()) {
            if (!item.isDirectory)
                yield item;
        }
    }

    async* forEachFileRecursively() {
        for await (let item of this.ls()) {
            if (item.isDirectory) {
                for await (let file of item.entry.forEachFileRecursively())
                    yield file;
            } else {
                yield item;
            }
        }
    }

    async* forEachDirectoryRecursively() {
        for await (let item of this.ls()) {
            if (!item.isDirectory)
                continue;

            for await (let dirItem of item.entry.forEachDirectoryRecursively())
                yield dirItem;

            yield item;
        }
    }

    async fileCount() {
        let count = 0;
        for await (let item of this.ls()) {
            if (!item.isDirectory)
                ++count;
        }

        return count;
    }

    async totalFileSize() {
        let size = 0;
        for await (const {entry: file} of this.forEachFileRecursively()) {
            size += file.byteLength;
        }
        return size;
    }

    async rm(name) {
        return this.structure.delete(name);
    }

    async visit(visitor) {
        visitor.visitDir(undefined, this);
        for await (const {name, entry, isDirectory} of this.ls()) {
            if (isDirectory)
                await entry.visit(visitor);
            else
                visitor.visitFile(name, entry);
        }
    }

}

const MAX_DIR_COUNT = 2500;
const MAX_FILE_COUNT = 800;

async function setupDirectory() {
    const fs = new Directory;
    let dirs = [fs];
    let counter = 0;
    for (let dir of dirs) {
        for (let i = 0; i < 15; ++i) {
            if (dirs.length <= MAX_DIR_COUNT) {
                dirs.push(await dir.addDirectory(`dir-${i}`));
            }
            counter++;
        }
    }

    let fileCounter = 0;
    for await (const fileContents of randomFileContents()) {
        const dirIndex = fileCounter * 107;
        const dir = dirs[dirIndex % dirs.length];
        await dir.addFile(`file-${fileCounter}`, new File(fileContents));
        fileCounter++
        if (fileCounter >= MAX_FILE_COUNT)
            break;
    }

    return fs;
}

class FSVisitor {
    visitFile(name, file) {
    }

    visitDir(name, dir) {
    }
}

class CountVisitor extends FSVisitor {
    fileCount = 0;
    dirCount = 0;

    visitFile() {
        this.fileCount++;
    }

    visitDir() {
        this.dirCount++;
    }
}

class CountDracula extends FSVisitor {
    bytes = 0;
    visitFile(name, file) {
        this.bytes += file.byteLength;
    }
}


class Benchmark {
    EXPECTED_FILE_COUNT = 739;

    totalFileCount = 0;
    totalDirCount = 0;
    lastFileHash = undefined;
    fs;

    async prepareForNextIteration() {
        this.fs = await setupDirectory();
    }

    async runIteration() {
        for await (let { entry: file } of this.fs.forEachFileRecursively()) {
            this.lastFileHash = file.swapByteOrder();
        }

        let bytesDeleted = 0;
        let counter = 0;
        for await (const { name, entry: dir } of this.fs.forEachDirectoryRecursively()) {
            const oldTotalSize = await dir.totalFileSize();
            if (await dir.fileCount() === 0)
                continue;
            counter++;
            if (counter % 13 !== 0)
                continue;
            for await (const { name } of dir.forEachFile()) {
                const result = await dir.rm(name);
                if (!result)
                    throw new Error("rm should have returned true");
            }
            const totalReducedSize = oldTotalSize - dir.totalFileSize();
            bytesDeleted += totalReducedSize;
        }
        if (bytesDeleted === 0)
            throw new Error("Did not delete any files");

        const countVisitor = new CountVisitor();
        await this.fs.visit(countVisitor);

        const countDracula = new CountDracula();
        await this.fs.visit(countDracula);

        let fileCount = 0;
        let fileBytes = 0;
        for await (const {entry: file} of this.fs.forEachFileRecursively()) {
            fileCount++
            fileBytes += file.byteLength;
        }
        this.totalFileCount += fileCount;

        let dirCount = 1;
        for await (let _ of this.fs.forEachDirectoryRecursively()) {
            dirCount++;
        }
        this.totalDirCount += dirCount;

        if (countVisitor.fileCount !== fileCount)
            throw new Error(`Invalid total file count ${countVisitor.fileCount}, expected ${fileCount}.`);
        if (countDracula.bytes !== fileBytes)
            throw new Error(`Invalid total file bytes ${countDracula.bytes}, expected ${fileBytes}.`);
        if (countVisitor.dirCount !== dirCount)
            throw new Error(`Invalid total dir count ${countVisitor.dirCount}, expected ${dirCount}.`);

    }

    validate(iterations) {
        const expectedFileCount = this.EXPECTED_FILE_COUNT * iterations;
        if (this.totalFileCount != expectedFileCount)
            throw new Error(`Invalid total file count ${this.totalFileCount}, expected ${expectedFileCount}.`);
        if (this.lastFileHash === undefined)
            throw new Error(`Invalid file hash: ${this.lastFileHash}`);
    }
}
